type TimeoutId = ReturnType<typeof setTimeout>;
type IntervalId = ReturnType<typeof setInterval>;

const DEFAULT_HEARTBEAT = {
    // 心跳间隔（毫秒)，0 禁用心跳
    interval: 30000,
    // 心跳消息内容
    message: 'ping',
    // 心跳响应超时时间（毫秒）
    timeout: 5000,
};

const DEFAULT_RECONNECT = {
    // 最大重连次数, 0 禁用重连
    maxAttempts: 3,
    // 重连间隔（毫秒）
    interval: 1000,
    // 重连间隔增长因子（例如设为1.5则每次重连间隔增加50%）
    growthFactor: 1.5
};

type EventCallbacks = {
    open?: (event: Event) => void;
    message?: (event: MessageEvent) => void;
    close?: (event: CloseEvent) => void;
    error?: (event: Event) => void;
    reconnect?: (attempt: number) => void;
};

type Options = {
    // 心跳配置
    heartbeat?: Partial<typeof DEFAULT_HEARTBEAT>;
    // 重连配置
    reconnect?: Partial<typeof DEFAULT_RECONNECT>;
};

export class WebSocketClient {
    // 连接地址
    private readonly url: string;
    // ws 实例
    private ws: WebSocket | null = null;
    // 事件回调
    private callbacks: EventCallbacks;
    // 配置
    private options: Options;
    // 是否销毁
    private isDestroyed: boolean = false;
    // 重连次数
    private reconnectAttempts: number = 0;
    // 心跳定时器
    private heartbeatTimer: IntervalId | null = null;
    // 心跳超时定时器
    private heartbeatTimeout: TimeoutId | null = null;

    constructor(url: string, callbacks: EventCallbacks = {}, options: Options = {}) {
        this.url = url;
        this.callbacks = callbacks;
        this.options = {
            heartbeat: {...DEFAULT_HEARTBEAT, ...options.heartbeat},
            reconnect: {...DEFAULT_RECONNECT, ...options.reconnect}
        };
        this.connect();
    }

    private connect() {
        if (this.isDestroyed) return;

        this.ws = new WebSocket(this.url);

        this.ws.onopen = (event) => {
            this.reconnectAttempts = 0;
            this.startHeartbeat();
            this.callbacks.open?.(event);
        };

        this.ws.onmessage = (event) => {
            if (isHeartbeatResponse(event.data)) {
                clearTimeout(this.heartbeatTimeout as TimeoutId);
                this.heartbeatTimeout = null;
            } else {
                this.callbacks.message?.(event);
            }
        };

        this.ws.onclose = (event) => {
            this.stopHeartbeat();
            this.callbacks.close?.(event);

            if (!this.isDestroyed) {
                this.handleReconnect();
            }
        };

        this.ws.onerror = (event) => {
            this.callbacks.error?.(event);
            if (!this.isDestroyed && this.ws?.readyState !== WebSocket.OPEN) {
                this.handleReconnect();
            }
        };
    }

    private startHeartbeat() {
        if (this.isDestroyed || !this.ws || this.options.heartbeat.interval <= 0) {
            return;
        }

        this.stopHeartbeat();

        this.heartbeatTimer = setInterval(() => {
            if (this.ws?.readyState === WebSocket.OPEN) {
                this.ws.send(this.options.heartbeat.message);
                console.log('WebSocket heartbeat sent');

                // 设置心跳超时定时器
                this.heartbeatTimeout = setTimeout(() => {
                    console.warn('WebSocket heartbeat timeout');
                    this.stopHeartbeat();
                    this.ws?.close();
                }, this.options.heartbeat.timeout);
            }
        }, this.options.heartbeat.interval);
    }

    private stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
        if (this.heartbeatTimeout) {
            clearTimeout(this.heartbeatTimeout);
            this.heartbeatTimeout = null;
        }
    }

    private handleReconnect() {
        if (
            this.isDestroyed ||
            this.options.reconnect.maxAttempts <= 0 ||
            this.reconnectAttempts >= this.options.reconnect.maxAttempts
        ) {
            return;
        }

        const {interval, growthFactor} = this.options.reconnect;
        const delay = interval * Math.pow(growthFactor, this.reconnectAttempts);

        this.reconnectAttempts++;
        this.callbacks.reconnect?.(this.reconnectAttempts);

        setTimeout(() => {
            if (!this.isDestroyed) {
                this.connect();
            }
        }, delay);
    }

    public send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
        if (this.ws?.readyState === WebSocket.OPEN) {
            this.ws.send(data);
        } else {
            console.warn('WebSocket is not open. Message not sent.');
        }
    }

    public destroy(code?: number, reason?: string) {
        this.isDestroyed = true;
        this.stopHeartbeat();
        if (this.ws) {
            this.ws.close(code, reason);
        }
    }

    public reconnect() {
        if (this.ws) {
            this.ws.close();
        }
        this.reconnectAttempts = 0;
        this.connect();
    }

    public getReadyState(): number {
        return this.ws?.readyState ?? WebSocket.CLOSED;
    }
}

/**
 * 是否是心跳响应
 * @param message
 */
const isHeartbeatResponse = (message: string) => {
    try {
        return message === 'pong';
    } catch (error) {
        return false;
    }
}